• Steven Ponce
  • About
  • Data Visualizations
  • Projects
  • Resume
  • Email

On this page

  • Steps to Create this Graphic
    • 1. Load Packages & Setup
    • 2. Read in the Data
    • 3. Examine the Data
    • 4. Tidy Data
    • 5. Visualization Parameters
    • 6. Map
    • 7. Save
    • 8. Session Info
    • 9. GitHub Repository
    • 10. References

Geographic Distribution of U.S. Law Enforcement Agencies

  • Show All Code
  • Hide All Code

  • View Source

Showing 17,157 agencies across seven categories. City and County agencies account for 83% of all agencies.

TidyTuesday
Data Visualization
R Programming
2025
A detailed visualization examining the geographic distribution of U.S. law enforcement agencies across seven distinct categories, highlighting the dominance of City and County agencies while revealing unique regional patterns in the distribution of specialized law enforcement entities.
Author

Steven Ponce

Published

February 14, 2025

Figure 1: Seven small maps of the continental United States show law enforcement agencies’ geographic distribution by type. City agencies (11,251) and County agencies (3,023) show the densest coverage across the country. State Police (882) are concentrated in the Northeast. Other State Agencies (715) cluster in the Southeast. University/College agencies (671) are spread across the country, with higher density in the East. Other agencies (430) show scattered distribution. Tribal agencies (185) are primarily located in Western states.

Steps to Create this Graphic

1. Load Packages & Setup

Show code
## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
    tidyverse,      # Easily Install and Load the 'Tidyverse'
    ggtext,         # Improved Text Rendering Support for 'ggplot2'
    showtext,       # Using Fonts More Easily in R Graphs
    janitor,        # Simple Tools for Examining and Cleaning Dirty Data
    skimr,          # Compact and Flexible Summaries of Data
    scales,         # Scale Functions for Visualization
    glue,           # Interpreted String Literals
    here,           # A Simpler Way to Find Your Files
    camcorder,      # Record Your Plot History 
    maps            # Draw Geographical Maps
    )
})

### |- figure size ----
gg_record(
    dir    = here::here("temp_plots"),
    device = "png",
    width  =  8,
    height =  7,
    units  = "in",
    dpi    = 320
)

# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))

2. Read in the Data

Show code
tt <- tidytuesdayR::tt_load(2025, week = 07) 

agencies <- tt$agencies |> clean_names()

tidytuesdayR::readme(tt)
rm(tt)

3. Examine the Data

Show code
glimpse(agencies)
skim(agencies)

4. Tidy Data

Show code
# Define constants
continental_bounds <- list(
    states_exclude = c("alaska", "hawaii"),
    long = c(-125, -65),
    lat = c(25, 50),
    outlier_thresh = -125
)

# Get continental US map data
continental_states <- map_data("state") |>
    filter(!region %in% continental_bounds$states_exclude)

# Function to check if point is within continental bounds
is_continental <- function(long, lat, bounds) {
    between(long, bounds$long[1], bounds$long[2]) &
        between(lat, bounds$lat[1], bounds$lat[2])
}

# Process agencies data
cleaned_agencies <- agencies |>
    # Initial filtering
    filter(
        !agency_type %in% c(NA, "Unknown", "NA"),
        !state %in% str_to_title(continental_bounds$states_exclude),
        !(agency_type == "Other" & longitude < continental_bounds$outlier_thresh)
    ) |>
    # Filter to continental bounds
    filter(is_continental(longitude, latitude, continental_bounds))

# Calculate agency counts and create labels
agency_counts <- cleaned_agencies |>
    count(agency_type) |>
    arrange(desc(n)) |>
    mutate(
        label = str_glue("{agency_type}\n(n = {format(n, big.mark=',')})"), 
        pct_total = n/sum(n) * 100,
        agency_type = factor(agency_type, levels = agency_type)
    )

# Prepare final dataset for plotting
filtered_agencies <- cleaned_agencies |>
    left_join(agency_counts |> select(agency_type, label),
              by = "agency_type") |>
    mutate(
        # Set factor levels based on counts
        agency_type = factor(agency_type, 
                             levels = levels(agency_counts$agency_type)),
        # Create labels for faceting
        agency_label = factor(agency_type,
                              levels = levels(agency_type),
                              labels = agency_counts$label)
    )

# Extract total for use in plot
total_agencies <- agency_counts$total_agencies[1]

# Calculate summary statistics 
summary_stats <- list(
    total_agencies = sum(agency_counts$n),
    num_categories = n_distinct(filtered_agencies$agency_type),
    city_county_pct = round(100 * sum(agency_counts$n[1:2])/sum(agency_counts$n))
)

5. Visualization Parameters

Show code
### |-  plot aesthetics ----
# Get base colors with custom palette
colors <- get_theme_colors(
    palette = c(
        "City"                  = "#AD225E",         
        "County"                = "#D67195",       
        "State Police"          = "#228B7D",  
        "Other State Agency"    = "#B8860B", 
        "University or College" = "#2D439E",
        "Other"                 = "#8B4513",         
        "Tribal"                = "#666666"         
    )
)

### |-  titles and caption ----
title_text <- str_glue("Geographic Distribution of U.S. Law Enforcement Agencies")

subtitle_text <-  str_glue(
    "Showing **{format(summary_stats$total_agencies, big.mark=',')}** agencies across {summary_stats$num_categories} categories<br>",
    "**City** and **County** agencies account for **{summary_stats$city_county_pct}%** of all agencies"
)

# Create caption
caption_text <- create_social_caption(
    tt_year = 2025,
    tt_week = 07,
    source_text = "FBI Crime Data API"
)

### |-  fonts ----
setup_fonts()
fonts <- get_font_families()

### |-  plot theme ----

# Start with base theme
base_theme <- create_base_theme(colors)

# Add weekly-specific theme elements
weekly_theme <- extend_weekly_theme(
    base_theme,
    theme(
        # Remove axes
        axis.text = element_blank(),
        axis.title = element_blank(),
        panel.grid = element_blank(),
        
        # Style facet labels
        strip.text = element_text(
            size = rel(0.75),
            face = "bold",
            family = fonts$text,
            color = colors$title,
            margin = margin(b = 8, t = 8)
        ),
        
        # Add spacing
        panel.spacing = unit(1.2, "lines"),
        
        # Plot margins 
        plot.margin = margin(10, 10, 10, 10),
    )
)

# Set theme
theme_set(weekly_theme)

6. Map

Show code
### |-  Map  ----
p <- ggplot() +
    # Geoms
    geom_polygon(
        data = continental_states, 
        aes(x = long, y = lat, group = group),
        fill = "gray95",     
        color = "gray80", 
        linewidth = 0.3
    ) +
    geom_point(
        data = filtered_agencies,
        aes(x = longitude, 
            y = latitude,
            color = agency_type,
            alpha = agency_type),  
        size = 0.5
    ) +
    
    # Scales
    scale_alpha_manual(
        values = c(
            "City" = 0.4,
            "County" = 0.4,
            "Other" = 0.7,
            "Other State Agency" = 0.7,
            "State Police" = 0.7,
            "Tribal" = 0.8,    
            "University or College" = 0.7
        ),
        guide = "none"
    ) +
    scale_color_manual(
        values = colors$palette,
        guide = "none"
    ) +
    coord_fixed(
        1.3,
        xlim = continental_bounds$long,
        ylim = continental_bounds$lat
    ) +
    
    # Labs
    labs(
        x = NULL,
        y = NULL,
        title    = title_text,
        subtitle = subtitle_text,
        caption  = caption_text,
    ) +
    
    # Facets 
    facet_wrap(
        ~agency_label, 
        ncol = 3,
        scales = "fixed"
    ) +
    
    # Theme
    theme(
        plot.title = element_text(
            size = rel(1.3),
            family = fonts$title,
            face = "bold",
            color = colors$title,
            lineheight = 1.1,
            margin = margin(t = 0, b = 8)
        ),
        plot.subtitle = element_markdown(
            size = rel(0.7),
            lineheight = (1.2),
            family = fonts$subtitle,
            color = colors$subtitle,
            margin = margin(b = 15)
        ),
        plot.caption = element_markdown(
            size   = rel(0.5),
            family = fonts$caption,
            color  = colors$caption,
            hjust  = 1,
            margin = margin(t = 0, b = 0)
        )
    )

7. Save

Show code
### |-  plot image ----  
save_plot(
  plot = p, 
  type = "tidytuesday", 
  year = 2025, 
  week = 7, 
  width = 8,
  height = 7
)

8. Session Info

Expand for Session Info
R version 4.4.1 (2024-06-14 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22631)

Matrix products: default


locale:
[1] LC_COLLATE=English_United States.utf8 
[2] LC_CTYPE=English_United States.utf8   
[3] LC_MONETARY=English_United States.utf8
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.utf8    

time zone: America/New_York
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
 [1] maps_3.4.2.1    camcorder_0.1.0 here_1.0.1      glue_1.8.0     
 [5] scales_1.3.0    skimr_2.1.5     janitor_2.2.0   showtext_0.9-7 
 [9] showtextdb_3.0  sysfonts_0.8.9  ggtext_0.1.2    lubridate_1.9.3
[13] forcats_1.0.0   stringr_1.5.1   dplyr_1.1.4     purrr_1.0.2    
[17] readr_2.1.5     tidyr_1.3.1     tibble_3.2.1    ggplot2_3.5.1  
[21] tidyverse_2.0.0 pacman_0.5.1   

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       httr2_1.0.6        xfun_0.49          htmlwidgets_1.6.4 
 [5] gh_1.4.1           tzdb_0.4.0         vctrs_0.6.5        tools_4.4.0       
 [9] generics_0.1.3     parallel_4.4.0     curl_6.0.0         gifski_1.32.0-1   
[13] fansi_1.0.6        pkgconfig_2.0.3    lifecycle_1.0.4    farver_2.1.2      
[17] compiler_4.4.0     textshaping_0.4.0  munsell_0.5.1      repr_1.1.7        
[21] codetools_0.2-20   snakecase_0.11.1   htmltools_0.5.8.1  yaml_2.3.10       
[25] crayon_1.5.3       pillar_1.9.0       magick_2.8.5       commonmark_1.9.2  
[29] tidyselect_1.2.1   digest_0.6.37      stringi_1.8.4      labeling_0.4.3    
[33] rsvg_2.6.1         rprojroot_2.0.4    fastmap_1.2.0      grid_4.4.0        
[37] colorspace_2.1-1   cli_3.6.3          magrittr_2.0.3     base64enc_0.1-3   
[41] utf8_1.2.4         withr_3.0.2        rappdirs_0.3.3     bit64_4.5.2       
[45] timechange_0.3.0   rmarkdown_2.29     tidytuesdayR_1.1.2 gitcreds_0.1.2    
[49] bit_4.5.0          ragg_1.3.3         hms_1.1.3          evaluate_1.0.1    
[53] knitr_1.49         markdown_1.13      rlang_1.1.4        gridtext_0.1.5    
[57] Rcpp_1.0.13-1      xml2_1.3.6         renv_1.0.3         vroom_1.6.5       
[61] svglite_2.1.3      rstudioapi_0.17.1  jsonlite_1.8.9     R6_2.5.1          
[65] systemfonts_1.1.0 

9. GitHub Repository

Expand for GitHub Repo

The complete code for this analysis is available in tt_2025_07.qmd.

For the full repository, click here.

10. References

Expand for References
  1. Data Sources:
    • TidyTuesday 2025 Week 07]: Agencies from the FBI Crime Data API
Back to top
Source Code
---
title: "Geographic Distribution of U.S. Law Enforcement Agencies"
subtitle: "Showing 17,157 agencies across seven categories. City and County agencies account for 83% of all agencies. "
description: "A detailed visualization examining the geographic distribution of U.S. law enforcement agencies across seven distinct categories, highlighting the dominance of City and County agencies while revealing unique regional patterns in the distribution of specialized law enforcement entities."
author: "Steven Ponce"
date: "2025-02-14" 
categories: ["TidyTuesday", "Data Visualization", "R Programming", "2025"]
tags: [
   "maps",
   "law-enforcement",
   "geographic-visualization",
   "small-multiples",
   "fbi-data",
   "spatial-analysis",
   "data-cleaning",
   "tidyverse"
]
image: "thumbnails/tt_2025_07.png"
format:
  html:
    toc: true
    toc-depth: 5
    code-link: true
    code-fold: true
    code-tools: true
    code-summary: "Show code"
    self-contained: true
    theme: 
      light: [flatly, assets/styling/custom_styles.scss]
      dark: [darkly, assets/styling/custom_styles_dark.scss]
editor_options: 
  chunk_output_type: inline
execute: 
  freeze: true                                                  
  cache: true                                                   
  error: false
  message: false
  warning: false
  eval: true
# filters:
#   - social-share
# share:
#   permalink: "https://stevenponce.netlify.app/data_visualizations/TidyTuesday/2025/tt_2025_07.html"
#   description: "Explore the geographic distribution of U.S. law enforcement agencies through a series of small multiples maps, revealing patterns in the placement of different agency types across the continental United States. #TidyTuesday #rstats #chart"
# 
#   twitter: true
#   linkedin: true
#   email: true
#   facebook: false
#   reddit: false
#   stumble: false
#   tumblr: false
#   mastodon: true
#   bsky: true
---

![Seven small maps of the continental United States show law enforcement agencies' geographic distribution by type. City agencies (11,251) and County agencies (3,023) show the densest coverage across the country. State Police (882) are concentrated in the Northeast. Other State Agencies (715) cluster in the Southeast. University/College agencies (671) are spread across the country, with higher density in the East. Other agencies (430) show scattered distribution. Tribal agencies (185) are primarily located in Western states.](tt_2025_07.png){#fig-1}


### <mark> **Steps to Create this Graphic** </mark>

#### 1. Load Packages & Setup

```{r}
#| label: load
#| warning: false
#| message: false      
#| results: "hide"     

## 1. LOAD PACKAGES & SETUP ----
suppressPackageStartupMessages({
if (!require("pacman")) install.packages("pacman")
pacman::p_load(
    tidyverse,      # Easily Install and Load the 'Tidyverse'
    ggtext,         # Improved Text Rendering Support for 'ggplot2'
    showtext,       # Using Fonts More Easily in R Graphs
    janitor,        # Simple Tools for Examining and Cleaning Dirty Data
    skimr,          # Compact and Flexible Summaries of Data
    scales,         # Scale Functions for Visualization
    glue,           # Interpreted String Literals
    here,           # A Simpler Way to Find Your Files
    camcorder,      # Record Your Plot History 
    maps            # Draw Geographical Maps
    )
})

### |- figure size ----
gg_record(
    dir    = here::here("temp_plots"),
    device = "png",
    width  =  8,
    height =  7,
    units  = "in",
    dpi    = 320
)

# Source utility functions
suppressMessages(source(here::here("R/utils/fonts.R")))
source(here::here("R/utils/social_icons.R"))
source(here::here("R/utils/image_utils.R"))
source(here::here("R/themes/base_theme.R"))
```

#### 2. Read in the Data

```{r}
#| label: read
#| include: true
#| eval: true
#| warning: false

tt <- tidytuesdayR::tt_load(2025, week = 07) 

agencies <- tt$agencies |> clean_names()

tidytuesdayR::readme(tt)
rm(tt)
```

#### 3. Examine the Data

```{r}
#| label: examine
#| include: true
#| eval: true
#| results: 'hide'
#| warning: false

glimpse(agencies)
skim(agencies)
```

#### 4. Tidy Data

```{r}
#| label: tidy
#| warning: false

# Define constants
continental_bounds <- list(
    states_exclude = c("alaska", "hawaii"),
    long = c(-125, -65),
    lat = c(25, 50),
    outlier_thresh = -125
)

# Get continental US map data
continental_states <- map_data("state") |>
    filter(!region %in% continental_bounds$states_exclude)

# Function to check if point is within continental bounds
is_continental <- function(long, lat, bounds) {
    between(long, bounds$long[1], bounds$long[2]) &
        between(lat, bounds$lat[1], bounds$lat[2])
}

# Process agencies data
cleaned_agencies <- agencies |>
    # Initial filtering
    filter(
        !agency_type %in% c(NA, "Unknown", "NA"),
        !state %in% str_to_title(continental_bounds$states_exclude),
        !(agency_type == "Other" & longitude < continental_bounds$outlier_thresh)
    ) |>
    # Filter to continental bounds
    filter(is_continental(longitude, latitude, continental_bounds))

# Calculate agency counts and create labels
agency_counts <- cleaned_agencies |>
    count(agency_type) |>
    arrange(desc(n)) |>
    mutate(
        label = str_glue("{agency_type}\n(n = {format(n, big.mark=',')})"), 
        pct_total = n/sum(n) * 100,
        agency_type = factor(agency_type, levels = agency_type)
    )

# Prepare final dataset for plotting
filtered_agencies <- cleaned_agencies |>
    left_join(agency_counts |> select(agency_type, label),
              by = "agency_type") |>
    mutate(
        # Set factor levels based on counts
        agency_type = factor(agency_type, 
                             levels = levels(agency_counts$agency_type)),
        # Create labels for faceting
        agency_label = factor(agency_type,
                              levels = levels(agency_type),
                              labels = agency_counts$label)
    )

# Extract total for use in plot
total_agencies <- agency_counts$total_agencies[1]

# Calculate summary statistics 
summary_stats <- list(
    total_agencies = sum(agency_counts$n),
    num_categories = n_distinct(filtered_agencies$agency_type),
    city_county_pct = round(100 * sum(agency_counts$n[1:2])/sum(agency_counts$n))
)
```

#### 5. Visualization Parameters

```{r}
#| label: params
#| include: true
#| warning: false

### |-  plot aesthetics ----
# Get base colors with custom palette
colors <- get_theme_colors(
    palette = c(
        "City"                  = "#AD225E",         
        "County"                = "#D67195",       
        "State Police"          = "#228B7D",  
        "Other State Agency"    = "#B8860B", 
        "University or College" = "#2D439E",
        "Other"                 = "#8B4513",         
        "Tribal"                = "#666666"         
    )
)

### |-  titles and caption ----
title_text <- str_glue("Geographic Distribution of U.S. Law Enforcement Agencies")

subtitle_text <-  str_glue(
    "Showing **{format(summary_stats$total_agencies, big.mark=',')}** agencies across {summary_stats$num_categories} categories<br>",
    "**City** and **County** agencies account for **{summary_stats$city_county_pct}%** of all agencies"
)

# Create caption
caption_text <- create_social_caption(
    tt_year = 2025,
    tt_week = 07,
    source_text = "FBI Crime Data API"
)

### |-  fonts ----
setup_fonts()
fonts <- get_font_families()

### |-  plot theme ----

# Start with base theme
base_theme <- create_base_theme(colors)

# Add weekly-specific theme elements
weekly_theme <- extend_weekly_theme(
    base_theme,
    theme(
        # Remove axes
        axis.text = element_blank(),
        axis.title = element_blank(),
        panel.grid = element_blank(),
        
        # Style facet labels
        strip.text = element_text(
            size = rel(0.75),
            face = "bold",
            family = fonts$text,
            color = colors$title,
            margin = margin(b = 8, t = 8)
        ),
        
        # Add spacing
        panel.spacing = unit(1.2, "lines"),
        
        # Plot margins 
        plot.margin = margin(10, 10, 10, 10),
    )
)

# Set theme
theme_set(weekly_theme)
```

#### 6. Map

```{r}
#| label: plot
#| warning: false

### |-  Map  ----
p <- ggplot() +
    # Geoms
    geom_polygon(
        data = continental_states, 
        aes(x = long, y = lat, group = group),
        fill = "gray95",     
        color = "gray80", 
        linewidth = 0.3
    ) +
    geom_point(
        data = filtered_agencies,
        aes(x = longitude, 
            y = latitude,
            color = agency_type,
            alpha = agency_type),  
        size = 0.5
    ) +
    
    # Scales
    scale_alpha_manual(
        values = c(
            "City" = 0.4,
            "County" = 0.4,
            "Other" = 0.7,
            "Other State Agency" = 0.7,
            "State Police" = 0.7,
            "Tribal" = 0.8,    
            "University or College" = 0.7
        ),
        guide = "none"
    ) +
    scale_color_manual(
        values = colors$palette,
        guide = "none"
    ) +
    coord_fixed(
        1.3,
        xlim = continental_bounds$long,
        ylim = continental_bounds$lat
    ) +
    
    # Labs
    labs(
        x = NULL,
        y = NULL,
        title    = title_text,
        subtitle = subtitle_text,
        caption  = caption_text,
    ) +
    
    # Facets 
    facet_wrap(
        ~agency_label, 
        ncol = 3,
        scales = "fixed"
    ) +
    
    # Theme
    theme(
        plot.title = element_text(
            size = rel(1.3),
            family = fonts$title,
            face = "bold",
            color = colors$title,
            lineheight = 1.1,
            margin = margin(t = 0, b = 8)
        ),
        plot.subtitle = element_markdown(
            size = rel(0.7),
            lineheight = (1.2),
            family = fonts$subtitle,
            color = colors$subtitle,
            margin = margin(b = 15)
        ),
        plot.caption = element_markdown(
            size   = rel(0.5),
            family = fonts$caption,
            color  = colors$caption,
            hjust  = 1,
            margin = margin(t = 0, b = 0)
        )
    )
```

#### 7. Save

```{r}
#| label: save
#| warning: false

### |-  plot image ----  
save_plot(
  plot = p, 
  type = "tidytuesday", 
  year = 2025, 
  week = 7, 
  width = 8,
  height = 7
)
```

#### 8. Session Info

::: {.callout-tip collapse="true"}
##### Expand for Session Info

```{r, echo = FALSE}
#| eval: true
#| warning: false

sessionInfo()
```
:::

#### 9. GitHub Repository

::: {.callout-tip collapse="true"}
##### Expand for GitHub Repo

The complete code for this analysis is available in [`tt_2025_07.qmd`](https://github.com/poncest/personal-website/blob/master/data_visualizations/TidyTuesday/2025/tt_2025_07.qmd).

For the full repository, [click here](https://github.com/poncest/personal-website/).
:::


#### 10. References
::: {.callout-tip collapse="true"}
##### Expand for References

1. Data Sources:
   - TidyTuesday 2025 Week 07]: [Agencies from the FBI Crime Data API](https://github.com/rfordatascience/tidytuesday/blob/main/data/2025/2025-02-18)

:::

© 2024 Steven Ponce

Source Issues